Earlier versions of iOS provided a few ways to render text in your own custom views: make a UILabel of your view, or tell an NSString
to draw its contents in a view. But these approaches are somewhat
limited. There's no way to specify font, color, or other attributes of a
substring to be rendered, for instance. So, if you wanted to render
some bold text within a paragraph, you would need to first render
everything that came before the bold text, determine the CGPoint
where the rendering ended, render the bold portion, calculate its end
point, start rendering the next piece, and so on. And, of course, at
each step, you would need to check to see if you're wrapping to the next
line; if so, you would need to start over from there with the rest of
the string.
In short, dealing with
long strings, or strings with varied styles throughout, has really been
kind of a pain in iOS. The level of complexity in laying out rich text
is almost on par with laying out a web page, which is the solution that
many iPhone applications are built around when displaying styled text,
using a UIWebView to do the layout. But there are problems with that approach as well. UIWebView is a fairly "heavy" class, which can take a while to load and display content—even content that's stored locally on the device.
With Core Text, you now have a chance to skip the web view, and just draw text directly into any graphics context you like. Figure 1 shows an example of some text rendered in Dudel using Core Text.
Note that Core Text is a fairly
low-level way to deal with a piece of text. iOS still doesn't offer
anything that is as versatile as the NSTextView
class in Mac OS X, which will also let you edit rich text, setting
fonts and colors as you like. The presence of Core Text is, however, a
good step in the right direction. And it's quite possible that Apple or a
third party will soon leverage it to provide a general-purpose GUI
class for editing rich text.
The Structure of Core Text
Before
we start making use of Core Text in our code, an overview of how it
works is in order. Unlike most of the new APIs discussed in the book,
Core Text is a C-based API, rather than a set of Objective-C classes.
For its "home environment" of Mac OS X, it was designed to be a unified
API that could be used easily from both Cocoa applications written in
Objective-C and Carbon applications written mainly in C and C++.
However, like most other modern C-based APIs present in Mac OS X and
iOS, Core Text is written in a way that is as close to
object-orientation with C as possible, using opaque types for all its
structures and accessing those structures only through a comprehensive
set of functions. So it's fairly painless.
Core Text allows you work with
it on a variety of levels. The simplest way lets you take a text string
and a rectangle, and with a few lines of code, have the text rendered
for you. If you want more fine-grained control, it's possible to reach
in and tweak the rendering a bit as well, but most of the time, the
high-level functionality is all you'll need. You'll access this through
an opaque type called CTFramesetter and its associated functions. You create a CTFramesetter by passing a special kind of string called an attributed string to the CTFramesetterCreateWithAttributedString() function, then create another Core Text object called a CTFrame by passing the CTFramesetter and a CGPath (containing just a rectangle) to the CTFramesetterCreateFrame() function, and finally render the result with a call to the CTFrameDraw() function. Figure 2 shows how these different pieces fit together.
An attributed string
consists of a string and some metadata describing formatting attributes
for portions of the string. For instance, you might want to render text
where some words are underlined, bold, or in a different font or color.
Attributed strings give you a concrete way to represent this sort of
thing. In iOS, attributed strings are represented by the Objective-C
class NSAttributedString and the C type CFAttributedStringRef. These are toll-free bridged to one another, so you can use them interchangeably.
In both iOS and Mac OS X, there are some kinds of entities that are said to be toll-free bridged
to one another. This typically refers to an Objective-C class and an
opaque C type. In a nutshell, this means that the types are equivalent,
and that any function or method that accepts one of them will work just
as well with the other (though you may need to do some manual casting to
satisfy the compiler). Many of the types that are used in the Core
Foundation C library, such as CFString, CFArray, and so on, are toll-free bridged to a similarly named counterpart in Objective-C's Foundation framework.
The Core Foundation types are
all opaque types, which means that each of them is really a pointer to a
structure whose contents you, as a user of the API, shouldn't be
concerned with. As a way of highlighting the fact that you're dealing
with references rather than with the structures themselves, the opaque
types defined in Core Foundation have the suffix Ref on the end of each type name. The following lines illustrate the concept of passing bridged types around.
NSString *objcString; // assume this exists. CFStringRef cfString; // this too.
functionThatWantsCFString(cfString); // this is fine, functionThatWantsCFString((CFStringRef)objcString); // and so is this!
[objcString length]; // this returns an integer, [(NSString *)cfString length]; // and so does this!
The opaque types created by
Core Foundation abide by the same memory management rules as Objective-C
objects do: You're required to release anything that you create or
retain. However, whenever you create these objects from C functions
instead of Objective-C methods, you should always manage them using
functions like CFRetain() and CFRelease(), instead of the retain and release
methods. This is partly to be stylistically consistent, but also
because some of them may not actually be Objective-C objects at all.
|
As an example, let's look at some code that creates an NSAttributedString from an NSString, and assigns a bold attribute to a part of the string.
NSString *myString = @"A dingo stole my baby!";
NSMutableAttributedString *attrString =
[[[NSMutableAttributedString alloc] initWithString:myString] autorelease];
[attrString addAttribute:(NSString *)(kCTForegroundColorAttributeName)
value:(id)[UIColor redColor].CGColor
range:NSMakeRange(0, [myString length])];
The specified attribute name, kCTForegroundColorAttributeName,
defines which attribute of the text we're setting, and then we pass in a
value to set for that attribute and the range of the text to cover. In
this case, we're just making it all red, but you can do whatever you
like. The Core Text String Attributes Reference,
included with the SDK, contains a list of all the attributes that
apply, their meanings, and the type of values expected for each. In
addition to color, you can set fonts, paragraph styles, and more.
Once you've created an attributed string, all you really need to do is create a CTFramesetter, use that to generate CTFrame, and tell it to draw itself.
CTFramesetterRef framesetter =
CTFramesetterCreateWithAttributedString((CFAttributedStringRef)attrString);
CTFrameRef frame = CTFramesetterCreateFrame(framesetter,
CFRangeMake(0, [attrString length]),
myCGPath, // the rect to draw into
NULL);
CTFrameDraw(frame, graphicsContext); // needs a graphics context to draw into
This code is taken out of
context, and will need some help before it will really do anything. The
next section will provide some context for it, in the form of a new Text
tool for Dudel.